package ch.elca.lol.puissance4.board;

import ch.elca.lol.puissance4.game.Token;
import ch.elca.lol.puissance4.util.TokenCounter;
import ch.elca.lol.puissance4.util.Util;

// TODO use fill ? completed rows can be shared between instances

/** Board implementation using a 1-d array to store tokens. */
public final class FastImBoard implements Board {
	private final BoardData tokens;
	private final int plays;
	private final Token winner;
	private final int lastMove;
	
	private FastImBoard(BoardData data, int lastMove, int plays, Token winner) {
		this.tokens = data;
		this.plays = plays;
		this.winner = winner;
		this.lastMove = lastMove;
	}
	
	public FastImBoard(int w, int h) {
		this(new LinMat(w, h), -1, 0, null);
	}
	
	// TODO: performance bottleneck is here
	@Override
	public Board play(int col, Token t) {
		if(!isPlayable(col))
			throw new RuntimeException("Tried to play invalid move [" + col + "].");
		
		int y = tokens.bottomOfCol(col);
		BoardData copy = tokens.set(col, y, t);
		
		// See whether this move was a win, otherwise set token to null for constructor
		if(!checkWin(copy, col, y, t))
			t = null;
		
		return new FastImBoard(copy, col, plays + 1, t);
	}
	
	@Override
	public boolean isGameOver() {
		return plays == getWidth() * getHeight() || getWinner() != null;
	}
	
	@Override
	public boolean isPlayable(int col) {
		return col >= 0 && col < getWidth() && tokens.isNull(col, getHeight() - 1);
	}
	
	@Override
	public Token unsafeGet(int x, int y) {
		return tokens.get(x, y);
	}
	
	public boolean hasWon(Token token) {
		return token == winner;
	}
	
	@Override
	public boolean isInBounds(int x, int y) {
		return x >= 0 && y >= 0 && x < getWidth() && y < getHeight();
	}
	
	@Override
	public boolean isEmpty(int x, int y) {
		return tokens.isNull(x, y);
	}
	
	@Override
	public int getWidth() {
		return tokens.getWidth();
	}
	
	@Override
	public int getHeight() {
		return tokens.getHeight();
	}
	
	@Override
	public Token get(int x, int y) {
		if(isInBounds(x, y))
			return unsafeGet(x, y);
		return null;
	}
	
	@Override
	public Token getWinner() {
		return winner;
	}
	
	/** Prints the board has a matrix of characters, X and O for played squares and . for empty ones. */
	@Override
	public String toString() {
		return Util.boardToString(this);
	}
	
	@Override
	public int getLastMove() {
		return lastMove;
	}
	
	@Override
	public int getColumnHeight(int col) {
		return tokens.bottomOfCol(col);
	}
	
	/** Check whether the token t, on top of column x generated a win. */
	private static boolean checkWin(BoardData copy, int x, int y, Token t) {
		final int d = 3;
		
		// Board size
		int w = copy.getWidth();
		int h = copy.getHeight();
		
		TokenCounter ctr = new TokenCounter();
		
		// Coordinates of the bounding box of size 7 centered in (x,y), taking into account
		// that the box isn't allowed to leave the board.
		int minY = Util.max(0, y - d);
		int maxY = Util.min(h - 1, y + d);
		int minX = Util.max(0, x - d);
		int maxX = Util.min(w - 1, x + d);
		
		// Check three tokens under for a vertical win
		for(int ty = minY; ty <= y; ty++) {
			if(ctr.increaseAndCheckFour(copy.get(x, ty), t))
				return true;
		}
		ctr.reset();
		
		// Check 3 tokens to the left, and three to the right for an horizontal win
		for(int tx = minX; tx <= maxX; tx++) {
			if(ctr.increaseAndCheckFour(copy.get(tx, y), t))
				return true;
		}
		ctr.reset();
		
		// Max distance from (x,y) on the bottom-left to top-right diagonal
		int bl = Util.max(minX - x, minY - y);
		int tr = Util.min(maxX - x, maxY - y);
		for(int r = bl; r <= tr; r++) {
			if(ctr.increaseAndCheckFour(copy.get(x + r, y + r), t))
				return true;
		}
		ctr.reset();
		
		// Max distance from (x,y) on the top-left to bottom-right diagonal
		int tl = Util.max(minX - x, y - maxY);
		int br = Util.min(maxX - x, y - minY);
		for(int r = tl; r <= br; r++) {
			if(ctr.increaseAndCheckFour(copy.get(x + r, y - r), t))
				return true;
		}
		
		return false;
	}
}
